<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of Melange_Framework
 *
 * PHP-HTTP is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-HTTP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-HTTP; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package aas
 *
 */

/**
 * AuthenticatorBase class
 *
 * <p>Basic implementation of the <b>Valve</b> interface that enforces the
 * <code><security-constraint></code> elements in the web application
 * deployment descriptor.  This functionality is implemented as a Valve
 * so that it can be ommitted in environments that do not require these
 * features.  Individual implementations of each supported authentication
 * method can subclass this base class as required.</p>
 * <p><b>USAGE CONSTRAINT</b>:  When this class is utilized, the Context to
 * which it is attached (or a parent Container in a hierarchy) must have an
 * associated Realm that can be used for authenticating users and enumerating
 * the roles to which they have been assigned.</p>
 * <p><b>USAGE CONSTRAINT</b>:  This Valve is only useful when processing HTTP
 * requests.  Requests of any other type will simply be passed through.</p>
 *
 * @category    Melange Framework
 * @package     aas
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 */
abstract class AuthenticatorBase {


    // -------------------------------------------------------------- Constants


    const BASIC_METHOD  = "BASIC";
    const CERT_METHOD   = "CLIENT-CERT";
    const DIGEST_METHOD = "DIGEST";
    const FORM_METHOD   = "FORM";
    const FORM_ACTION   = "/j_security_check";
    const FORM_PASSWORD = "j_password";
    const FORM_USERNAME = "j_username";


    /**
     * The notes key for the password used to authenticate this user.
     */
    const SESS_PASSWORD_NOTE =
      "nl.melange.authenticator.session.PASSWORD";


    /**
     * The notes key for the username used to authenticate this user.
     */
    const SESS_USERNAME_NOTE =
      "nl.melange.phpmvc.authenticator.USERNAME";


    /**
     * The previously authenticated principal (if caching is disabled).
     */
    const FORM_PRINCIPAL_NOTE =
        "nl.melange.phpmvc.authenticator.PRINCIPAL";


    /**
     * The original request information, to which the user will be
     * redirected if authentication succeeds.
     */
    const FORM_REQUEST_NOTE =
        "nl.melange.phpmvc.authenticator.REQUEST";



    // ----------------------------------------------------- Instance Variables


    /**
     * Should we cache authenticated Principals if the request is part of
     * an HTTP session?
     */
    protected $cache = true;


    /**
     * The Context to which this Valve is attached.
     * 
     * @var Context
     */
    protected $context = null;


    /**
     * Flag to determine if we disable proxy caching, or leave the issue
     * up to the webapp developer.
     */
    protected $disableProxyCaching = true;



    // ------------------------------------------------------------- Properties


    /**
     * Return the cache authenticated Principals flag.
     */
    public function getCache() {

        return ($this->cache);

    }


    /**
     * Set the cache authenticated Principals flag.
     *
     * @param cache The new cache flag
     */
    public function setCache($cache) {

        $this->cache = $cache;

    }


    /**
     * Return the Container to which this Valve is attached.
     */
    public function getContainer() {

        return ($this->context);

    }


    /**
     * Set the Container to which this Valve is attached.
     *
     * @param container The container to which we are attached
     */
    public function setContainer(Context $container) {

        if (!($container instanceof Context))
            throw new Exception("Configuration error:  Must be attached to a Context");

        $this->context = $container;
    }


    /**
     * Return the flag that states if we add headers to disable caching by
     * proxies.
     */
    public function getDisableProxyCaching() {
        return $this->disableProxyCaching;
    }


    /**
     * Set the value of the flag that states if we add headers to disable
     * caching by proxies.
     * @param nocache <code>true</code> if we add headers to disable proxy
     *              caching, <code>false</code> if we leave the headers alone.
     */
    public function setDisableProxyCaching($nocache) {
        $this->disableProxyCaching = $nocache;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Enforce the security restrictions in the web application deployment
     * descriptor of our associated Context.
     *
     * @param request Request to be processed
     * @param response Response to be processed
     * @param context The valve context used to invoke the next valve
     *  in the current processing pipeline
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if thrown by a processing element
     */
    public function invoke(Request $request, Response &$response) {

        if ($this->log->isDebugEnabled())
            $this->log->debug("Security checking request " .
                $request->getMethod() . " " .
                $request->getRequestURI());

        $config = $this->context->getLoginConfig();

        // Have we got a cached authenticated Principal to record?
        if ($this->cache) {
            $principal = $request->getUserPrincipal();
            if ($principal === null) {
                $session = $request->getSession(false);
                if ($session !== null) {
                    $principal = $session->getPrincipal();
                    if ($principal !== null) {
                        if ($this->log->isDebugEnabled())
                            $this->log->debug("We have cached auth type " .
                                $session->getAuthType() .
                                " for principal ");
                        $request->setAuthType($session->getAuthType());
                        $request->setUserPrincipal($principal);
                    }
                }
            }
        }

        // Special handling for form-based logins to deal with the case
        // where the login form (and therefore the "j_security_check" URI
        // to which it submits) might be outside the secured area
        $requestURI = urldecode($request->getRequestURI());

        if (preg_match("/\\".self::FORM_ACTION."$/", $requestURI)) {
            if (!$this->authenticate($request, $response, $config)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Failed authenticate() test");
                return;
            }
        }

        // Is this request URI subject to a security constraint?
        $constraint = $this->findConstraint($request);
        if (($constraint === null) /* &&
            (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) {
            if ($this->log->isDebugEnabled())
                $this->log->debug(" Not subject to any constraint");
            return;
        }
        if (($this->log->isDebugEnabled()) && ($constraint !== null))
            $this->log->debug(" Subject to constraint " . $constraint);

        // Make sure that constrained resources are not cached by web proxies
        // or browsers as caching can provide a security hole
        if ($this->disableProxyCaching &&
            !$request->isSecure() &&
            !"POST" == strtoupper($request->getMethod())) {
            $response->setHeader("Pragma", "No-cache");
            $response->setHeader("Cache-Control", "no-cache");
            $response->setDateHeader("Expires", 1);
        }

        // Enforce any user data constraint for this security constraint
//        if ($this->log->isDebugEnabled())
//            $this->log->debug(" Calling checkUserData()");
//        if (!checkUserData(hrequest, hresponse, constraint)) {
//            if ($this->log->isDebugEnabled())
//                $this->log->debug(" Failed checkUserData() test");
//            // ASSERT: Authenticator already set the appropriate
//            // HTTP status code, so we do not have to do anything special
//            return;
//        }

        // Authenticate based upon the specified login configuration
        if ($constraint->getAuthConstraint()) {
            if ($this->log->isDebugEnabled())
                    $this->log->debug(" Calling authenticate() based upon the specified login configuration");
            if (!$this->authenticate($request, $response, $config)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Failed authenticate() test");
                // ASSERT: Authenticator already set the appropriate
                // HTTP status code, so we do not have to do anything special
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" ---------------------------------- ");
                return;
            }
        }

        // Perform access control based on the specified role(s)
        if ($constraint->getAuthConstraint()) {
            if ($this->log->isDebugEnabled())
                    $this->log->debug(" Calling accessControl()");
            if (!$this->accessControl($request, $response, $constraint)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Failed accessControl() test");
                // ASSERT: AccessControl method has already set the appropriate
                // HTTP status code, so we do not have to do anything special
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" ---------------------------------- ");
                return;
            }
        }

        // Any and all specified constraints have been satisfied
        if ($this->log->isDebugEnabled())
            $this->log->debug(" Successfully passed all security constraints");

    }


    // ------------------------------------------------------ Protected Methods


    /**
     * Perform access control based on the specified authorization constraint.
     * Return <code>true</code> if this constraint is satisfied and processing
     * should continue, or <code>false</code> otherwise.
     *
     * @param request Request we are processing
     * @param response Response we are creating
     * @param constraint Security constraint we are enforcing
     *
     * @exception IOException if an input/output error occurs
     */
    protected function accessControl(Request $request,
                                    Response &$response,
                                    SecurityConstraint $constraint) {

        if ($constraint === null)
            return (true);

        // Specifically allow access to the form login and form error pages
        // and the "j_security_check" action
        $config = $this->context->getLoginConfig();
        if (($config !== null) &&
            (self::FORM_METHOD == $config->getAuthMethod())) {
            $requestURI = urldecode($request->getRequestURI());
            $loginPage = $this->context->getPath() . $config->getLoginPage();
            if ($loginPage == $requestURI) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Allow access to login page " . $loginPage);
                return (true);
            }
            $errorPage = $this->context->getPath() . $config->getErrorPage();
            if ($errorPage == $requestURI) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Allow access to error page " . $errorPage);
                return (true);
            }
            if (preg_match("/\\".self::FORM_ACTION."$/", $requestURI)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug(" Allow access to username/password submission");
                return (true);
            }
        }

        // Which user principal have we already authenticated?
        $principal = $request->getUserPrincipal();
        if ($principal === null) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("  No user authenticated, cannot grant access");
            $response->sendError(Response::SC_INTERNAL_SERVER_ERROR,
                "Configuration error:  Cannot perform access control without an authenticated principal");
            return (false);
        }

        // Check each role included in this constraint
        if ($constraint->getAllRoles()) {
            // * means all roles defined in web.xml
            $roles = $this->context->findSecurityRoles();
        } else {
            $roles = $constraint->findAuthRoles();
        }

        if ($roles === null)
            $roles = array();

        if ((count($roles) == 0) && ($constraint->getAuthConstraint())) {
            $response->sendError(Response::SC_FORBIDDEN, "Access to the requested resource has been denied");
            return (false); // No listed roles means no access at all
        }

        $realm = $this->context->getRealm();
        for ($i = 0; $i < count($roles); $i++) {
            if ($realm->hasRole($principal, $roles[$i]))
                return (true);
        }

        $response->sendError(Response::SC_FORBIDDEN, "Access to the requested resource has been denied");
        
        return (false);

    }


    /**
     * Authenticate the user making this request, based on the specified
     * login configuration.  Return <code>true</code> if any specified
     * constraint has been satisfied, or <code>false</code> if we have
     * created a response challenge already.
     *
     * @param request Request we are processing
     * @param response Response we are creating
     * @param config    Login configuration describing how authentication
     *              should be performed
     *
     * @exception IOException if an input/output error occurs
     */
    abstract protected function authenticate(Request $request, Response &$response,
                                            LoginConfig $config);


    /**
     * Return the SecurityConstraint configured to guard the request URI for
     * this request, or <code>null</code> if there is no such constraint.
     *
     * @param request Request we are processing
     */
    protected function findConstraint(Request $request) {

        // Are there any defined security constraints?
        $constraints = $this->context->findConstraints();
        if (($constraints === null) || (count($constraints) == 0)) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("  No applicable constraints defined");
            return (null);
        }

        // Check each defined security constraint
        $uri = urldecode($request->getRequestURI());
        $contextPath = $request->getContextPath();

        if (strlen($contextPath) > 0)
            $uri = substr($uri, strlen($contextPath));
           
        $method = $request->getMethod();

        for ($i = 0; $i < count($constraints); $i++) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("  Checking constraint '" . $constraints[$i] .
                    "' against " . $method . " " . $uri . " --> " .
                    $constraints[$i]->included($uri, $method));
            if ($constraints[$i]->included($uri, $method))
                return ($constraints[$i]);
        }

        // No applicable security constraint was found
        if ($this->log->isDebugEnabled())
                $this->log->debug("  No applicable constraint located");
        return (null);

    }


    /**
     * Register an authenticated Principal and authentication type in our
     * request, in the current session (if there is one), and with our
     * SingleSignOn valve, if there is one.  Set the appropriate cookie
     * to be returned.
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are generating
     * @param principal The authenticated Principal to be registered
     * @param authType The authentication type to be registered
     * @param username Username used to authenticate (if any)
     * @param password Password used to authenticate (if any)
     */
    protected function register(Request $request, GenericPrincipal $principal,
        $authType, $username, $password) {

        if ($this->log->isDebugEnabled())
            $this->log->debug("Authenticated '" . $principal->getName() . "' with type '"
                . $authType . "'");

        $p = clone $principal;

        // Cache the authentication information in our request
        $request->setAuthType($authType);
        $request->setUserPrincipal($p);

        $session = $request->getSession(false);

        // Cache the authentication information in our session, if any
        if ($this->cache) {

            if ($session !== null) {

                $session->setAuthType($authType);
                $session->setPrincipal($p);

                if ($username !== null) {
                    $session->setNote(self::SESS_USERNAME_NOTE, $username);
                } else {
                    $session->removeNote(self::SESS_USERNAME_NOTE);
                }

                if ($password !== null) {
                    $session->setNote(self::SESS_PASSWORD_NOTE, $password);
                } else {
                    $session->removeNote(self::SESS_PASSWORD_NOTE);
                }

            }
        }

    }

}

?>